第1章 GPU-Based Space Colonization Algorithm

This chapter introduces the GPU implementation of the Space Colonization Algorithm, an algorithm that generates a shape that blanchs along a point cloud, and its application examples.

The sample in this chapter is "Space Colonization" at
https://github.com/IndieVisualLab/UnityGraphicsProgramming4
.

SkinnedAnimation.scene

図1.1: SkinnedAnimation.scene

1.1  Introduction

The Space Colonization Algorithm was developed by Adam et al. * 1 as a tree modeling method.

[*1] http://algorithmicbotany.org/papers/colonization.egwnp2007.html

A method of generating a branching shape from a given point cloud,

It has the feature.

This chapter introduces the GPU implementation of this algorithm and application examples combined with skinning animation.

1.2  Algorithm

First, I will explain the Space Colonization Algorithm. The general steps of the algorithm are divided as follows.

  1. Setup-Initialization
  2. Search --Search for affected attractions
  3. Attract-Attracting branches
  4. Connect-Create a new node and connect to an existing node
  5. Remove --Remove Attraction
  6. Grow-Node Growth

1.2.1  Setup-Initialization

In the initialization phase, the point cloud is prepared as an attraction (point that will be the seed of the branch). Place one or more Nodes (branch branch points) in the attraction. This first placed Node will be the starting point for your branch.

In the figure below, Attraction is represented by a round dot and Node is represented by a square dot.

Setup --Attraction and Node initialization Round dots represent Attraction and square dots represent Node.

Figure 1.2: Setup --Attraction and Node Initialization Round dots represent Attraction and square dots represent Node.

1.2.2  Search-Search for affected Attractions

For each attraction, find the closest Node within the influence distance.

Search --Search the nearest Node within the range of influence from each attraction

Figure 1.3: Search-Search for the nearest Node in the area of ​​influence from each Attraction

1.2.3  Attract-Attracting branches

For each Node, determine the direction to extend the branch based on the attraction within the range of influence, and the point beyond the extension by the growth distance is the candidate point (Candidate) for the point to generate a new Node. )will do.

Attract-Extend a branch from each Node and decide a candidate point to generate a new Node

Figure 1.4: Attract-Extend a branch from each Node and determine candidate points to generate new Nodes

1.2.4  Connect-Connecting a new node to an existing node

Create a new Node at the Candidate position and connect the original Node with Edge to extend the branch.

Connect-Connect a new node to an existing node to extend a branch

Figure 1.5: Connect-Connecting a new node to an existing node to extend a branch

1.2.5 Remove --Remove  Attraction within the removal range

Deletes an attraction that is within the kill distance from the node.

Remove --Search Node for Attractions within the removal range

Figure 1.6: Remove --Search Node for attractions within the removal range

Remove-Removes Attraction found within the removal range

Figure 1.7: Remove-Removes Attraction found within the removal range

1.2.6 Grow --Node  Growth

Grow Node and go back to Step.2.

The general flow of the entire algorithm is shown in the figure below.

Rough flow of the algorithm

Figure 1.8: Rough flow of the algorithm

1.3  Implementation

Now, I will explain the concrete implementation of the algorithm.

1.3.1  Preparation of resources

As an element that increases or decreases in the Space Colonization Algorithm

Is required, but in order to express these on GPGPU, Append / ConsumeStructuredBuffer is used for some elements.

Append / ConsumeStructuredBuffer is explained in Unity Graphics Programming vol.3 "GPU-Based Cellular Growth Simulation".

Attraction (branch seed point)

The structure of Attraction is defined as follows.

Attraction.cs

public struct Attraction {
    public Vector3 position; // position
    public int nearest; // index of the nearest Node
    public uint found; // Whether a nearby Node was found
    public uint active; // Whether it is a valid attraction (1 is valid, 0 is deleted)
}

The increase / decrease of attraction is expressed by determining whether it is a deleted attraction by the active flag.

In Space Colonization, it is necessary to prepare a point cloud of Attraction in the initialization phase. In the sample SpaceColonization.cs, point clouds are randomly scattered inside the sphere and used as the position of Attraction.

SpaceColonization.cs

    // Randomly sprinkle points inside the sphere to generate an attraction
    var attractions = GenerateSphereAttractions();
    count = attractions.Length;

    // Initialize the Attraction buffer
    attractionBuffer = new ComputeBuffer(
        count,
        Marshal.SizeOf(typeof(Attraction)),
        ComputeBufferType.Default
    );
    attractionBuffer.SetData(attractions);

Node (branch branch point)

The structure of Node is defined as follows.

Node.cs

public struct Node {
    public Vector3 position; // position
    public float t; // Growth rate (0.0 ~ 1.0)
    public float offset; // Distance from Root (Node depth)
    public float mass; // mass
    public int from; // index of branch source Node
    public uint active; // Whether it is a valid Node (1 is valid)
}

Node resources

It is managed by two buffers.

SpaceColonization.cs

  // Actual Node data
  nodeBuffer = new ComputeBuffer(
    count,
    Marshal.SizeOf(typeof(Node)),
    ComputeBufferType.Default
  );

  // Object pool
  nodePoolBuffer = new ComputeBuffer(
    count,
    Marshal.SizeOf(typeof(int)),
    ComputeBufferType.Append
  );
  nodePoolBuffer.SetCounterValue(0);

Candidate (candidate for new Node)

The structure of Candidate is defined as follows.

Candidate.cs

public struct Candidate
{
    public Vector3 position; // position
    public int node; // index of the original Node of the candidate point
}

Candidate is represented by Append / ConsumeStructuredBuffer.

SpaceColonization.cs

    candidateBuffer = new ComputeBuffer(
        count,
        Marshal.SizeOf(typeof(Candidate)),
        ComputeBufferType.Append
    );
    candidateBuffer.SetCounterValue(0);

Edge (Edge that connects Nodes)

The structure of Edge is defined as follows.

Edge.cs

public struct Edge {
    public int a, b; // index of two Nodes connected by Edge
}

Edge is represented by Append / Consume Structured Buffer like Candidate.

SpaceColonization.cs

    edgeBuffer = new ComputeBuffer(
        count * 2,
        Marshal.SizeOf(typeof(Edge)),
        ComputeBufferType.Append
    );
    edgeBuffer.SetCounterValue(0);

1.3.2  Implementing the algorithm in Compute Shader

Now that we have the necessary resources, we will implement each step of the algorithm in GPGPU with Compute Shader.

Setup

In the initialization phase

to hold.

Pick up some from the prepared Attraction and generate an initial Node at that position.

SpaceColonization.cs

    var seeds = Enumerable.Range(0, seedCount).Select((_) => {
        return attractions[Random.Range(0, count)].position;
    }).ToArray();
    Setup(seeds);

SpaceColonization.cs

protected void Setup(Vector3[] seeds)
{
    var kernel = compute.FindKernel("Setup");
    compute.SetBuffer(kernel, "_NodesPoolAppend", nodePoolBuffer);
    compute.SetBuffer(kernel, "_Nodes", nodeBuffer);
    GPUHelper.Dispatch1D(compute, kernel, count);

    ...
}

The Setup kernel initializes the object pool. Store the index in the Node's object pool and turn off the active flag for that Node.

SpaceColonization.compute

void Setup (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;
  uint count, stride;
  _Nodes.GetDimensions(count, stride);
  if (idx >= count)
    return;

  _NodesPoolAppend.Append(idx);

  Node n = _Nodes[idx];
  n.active = false;
  _Nodes[idx] = n;
}

This will turn off the active flags for all Nodes and create an object pool with Node indexes.

Now that the object pool has been initialized, it's time to create the initial seed node.

The initial node is generated by executing the Seed kernel with the seed position (Vector3 []) prepared earlier as input.

SpaceColonization.cs

...

// seedBuffer is automatically disposed when it goes out of scope
using(
    ComputeBuffer seedBuffer = new ComputeBuffer(
        seeds.Length,
        Marshal.SizeOf(typeof(Vector3))
    )
)
{
    seedBuffer.SetData(seeds);
    kernel = compute.FindKernel("Seed");
    compute.SetFloat("_MassMin", massMin);
    compute.SetFloat("_MassMax", massMax);
    compute.SetBuffer(kernel, "_Seeds", seedBuffer);
    compute.SetBuffer(kernel, "_NodesPoolConsume", nodePoolBuffer);
    compute.SetBuffer(kernel, "_Nodes", nodeBuffer);
    GPUHelper.Dispatch1D(compute, kernel, seedBuffer.count);
}

// Initialize the number of Nodes and Edges
nodesCount = nodePoolBuffer.count;
edgesCount = 0;

...

The Seed kernel takes a position from the Seeds buffer and creates a Node at that position.

SpaceColonization.compute

void Seed (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;

  uint count, stride;
  _Seeds.GetDimensions(count, stride);
  if (idx >= count)
    return;

  Node n;

  // Create a new Node (see below)
  uint i = CreateNode(n);

  // Set Seed position to Node position
  n.position = _Seeds[idx];
  n.t = 1;
  n.offset = 0;
  n.from = -1;
  n.mass = lerp(_MassMin, _MassMax, nrand(id.xy));
  _Nodes[i] = n;
}

Create a new Node with the CreateNode function. Extracts the index from the object pool ConsumeStructuredBuffer and returns the initialized Node.

SpaceColonization.compute

uint CreateNode(out Node node)
{
  uint i = _NodesPoolConsume.Consume();
  node.position = float3(0, 0, 0);
  node.t = 0;
  node.offset = 0;
  node.from = -1;
  node.mass = 0;
  node.active = true;
  return i;
}

This is the end of the initialization phase.

Each step of the looping algorithm shown in Figure 1.8 is performed within the Step function.

SpaceColonization.cs

protected void Step(float dt)
{
    // Do not run when the object pool is empty
    if (nodesCount > 0)
    {
        Search();   // Step.2
        Attract();  // Step.3
        Connect();  // Step.4
        Remove();   // Step.5

        // Get the number of data that Append / ConsumeStructuredBuffer has
        CopyNodesCount();
        CopyEdgesCount();
    }
    Grow(dt);       // Step.6
}

Search

From each attraction, find the closest Node within the influence distance.

SpaceColonization.cs

protected void Search()
{
    var kernel = compute.FindKernel("Search");
    compute.SetBuffer(kernel, "_Attractions", attractionBuffer);
    compute.SetBuffer(kernel, "_Nodes", nodeBuffer);
    compute.SetFloat("_InfluenceDistance", unitDistance * influenceDistance);
    GPUHelper.Dispatch1D(compute, kernel, count);
}

The GPU kernel implementation is as follows.

SpaceColonization.compute

void Search (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;
  uint count, stride;
  _Attractions.GetDimensions(count, stride);
  if (idx >= count)
    return;

  Attraction attr = _Attractions[idx];

  attr.found = false;
  if (attr.active)
  {
    _Nodes.GetDimensions(count, stride);

    // Search for Nodes closer than influence distance
    float min_dist = _InfluenceDistance;

    // index of the nearest Node
    uint nearest = -1;

    // Execute a loop for all Nodes
    for (uint i = 0; i < count; i++)
    {
      Node n = _Nodes[i];

      if (n.active)
      {
        float3 dir = attr.position - n.position;
        float d = length(dir);
        if (d < min_dist)
        {
          // Update the nearest Node
          min_dist = d;
          nearest = i;

          // Set the index of the neighboring Node
          attr.found = true;
          attr.nearest = nearest;
        }
      }
    }

    _Attractions[idx] = attr;
  }
}

Attract

For each Node, determine the direction to extend the branch based on the attraction within the range of influence, and the point beyond the extension by the growth distance is the candidate point (Candidate) for the point to generate a new Node. )will do.

SpaceColonization.cs

protected void Attract()
{
    var kernel = compute.FindKernel("Attract");
    compute.SetBuffer(kernel, "_Attractions", attractionBuffer);
    compute.SetBuffer(kernel, "_Nodes", nodeBuffer);

    candidateBuffer.SetCounterValue (0); // Initialize the buffer that stores the candidate points
    compute.SetBuffer(kernel, "_CandidatesAppend", candidateBuffer);

    compute.SetFloat("_GrowthDistance", unitDistance * growthDistance);

    GPUHelper.Dispatch1D(compute, kernel, count);
}

The GPU kernel implementation is as follows. Please refer to the code contents and comments of the Attract kernel for the calculation method of the position of the candidate point.

SpaceColonization.compute

void Attract (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;
  uint count, stride;
  _Nodes.GetDimensions(count, stride);
  if (idx >= count)
    return;

  Node n = _Nodes[idx];

  // Node is valid and
  // Create a new Node if the growth rate (t) is greater than or equal to the threshold (1.0)
  if (n.active && n.t >= 1.0)
  {
    // Accumulation variable to extend the branch
    float3 dir = (0.0).xxx;
    uint counter = 0;

    // Run a loop for all attractions
    _Attractions.GetDimensions(count, stride);
    for (uint i = 0; i < count; i++)
    {
      Attraction attr = _Attractions[i];
      // Search for the attraction whose node is the nearest neighbor
      if (attr.active && attr.found && attr.nearest == idx)
      {
        // Normalize the vector from Node to Attraction and add it to the accumulation variable
        float3 dir2 = (attr.position - n.position);
        dir + = normalize (dir2);
        counter++;
      }
    }

    if (counter > 0)
    {
      Candidate c;

      // Take the average of the unit vectors from Node to Attraction
      // Set it as the position of the candidate point extended from the Node by the growth distance
      dir = dir / counter;
      c.position = n.position + (dir * _GrowthDistance);

      // Set the index of the original Node that extends to the candidate point
      c.node = idx;

      // Add to candidate point buffer
      _CandidatesAppend.Append(c);
    }
  }
}

Connect

Create a new Node based on the candidate point buffer generated by the Attract kernel, and extend the branch by connecting the Nodes with Edge.

In the Connect function, the number of kernel executions is determined by comparing the remaining number of object pools (nodesCount) with the size of the candidate point buffer so that data retrieval (Consume) is not executed when the Node object pool (nodePoolBuffer) is empty. I am.

SpaceColonization.cs

protected void Connect()
{
    var kernel = compute.FindKernel("Connect");
    compute.SetFloat("_MassMin", massMin);
    compute.SetFloat("_MassMax", massMax);
    compute.SetBuffer(kernel, "_Nodes", nodeBuffer);
    compute.SetBuffer(kernel, "_NodesPoolConsume", nodePoolBuffer);
    compute.SetBuffer(kernel, "_EdgesAppend", edgeBuffer);
    compute.SetBuffer(kernel, "_CandidatesConsume", candidateBuffer);

    // The number of data (nodeCount) of the Node object pool acquired by CopyNodeCount
    // Restrict so that it does not exceed
    var connectCount = Mathf.Min(nodesCount, CopyCount(candidateBuffer));
    if (connectCount > 0)
    {
        compute.SetInt("_ConnectCount", connectCount);
        GPUHelper.Dispatch1D(compute, kernel, connectCount);
    }
}

Below is the implementation of the GPU kernel.

SpaceColonization.compute

void Connect (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;
  if (idx >= _ConnectCount)
    return;

  // Extract candidate points from the candidate point buffer
  Candidate c = _CandidatesConsume.Consume();

  Node n1 = _Nodes [c.node];
  Node n2;

  // Generate Node at the position of the candidate point
  uint idx2 = CreateNode(n2);
  n2.position = c.position;
  n2.offset = n1.offset + 1.0; // Set the distance from Root (original Node + 1.0)
  n2.from = c.node; // Set the index of the original Node
  n2.mass = lerp(_MassMin, _MassMax, nrand(float2(c.node, idx2)));

  // Update Node buffer
  _Nodes[c.node] = n1;
  _Nodes[idx2] = n2;

  // Connect two Nodes with Edge (see below)
  CreateEdge(c.node, idx2);
}

The CreateEdge function creates an Edge based on the indexes of the two Nodes passed and adds it to the Edge buffer.

SpaceColonization.compute

void CreateEdge(int a, int b)
{
  Edge e;
  ea = a;
  e.b = b;
  _EdgesAppend.Append(e);
}

Remove

Remove the Attraction that is within the kill distance from the Node.

SpaceColonization.cs

protected void Remove()
{
    var kernel = compute.FindKernel("Remove");
    compute.SetBuffer(kernel, "_Attractions", attractionBuffer);
    compute.SetBuffer(kernel, "_Nodes", nodeBuffer);
    compute.SetFloat("_KillDistance", unitDistance * killDistance);
    GPUHelper.Dispatch1D(compute, kernel, count);
}

The GPU kernel implementation is as follows.

SpaceColonization.compute

void Remove(uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;
  uint count, stride;
  _Attractions.GetDimensions(count, stride);
  if (idx >= count)
    return;

  Attraction attr = _Attractions[idx];
  // Do not execute if the attraction has been deleted
  if (!attr.active)
    return;

  // Execute a loop for all Nodes
  _Nodes.GetDimensions(count, stride);
  for (uint i = 0; i < count; i++)
  {
    Node n = _Nodes[i];
    if (n.active)
    {
      // If there is a Node within the deletion range, turn off the active flag of Attraction and delete it
      float d = distance(attr.position, n.position);
      if (d < _KillDistance)
      {
        attr.active = false;
        _Attractions[idx] = attr;
        return;
      }
    }
  }
}

Grow

Grow Node.

When generating candidate points with the Attract kernel, it is used as a condition whether the growth rate (t) of Node is above the threshold value (if it is below the threshold value, no candidate points are generated), but the growth rate parameter is in this Grow kernel. I'm incrementing.

SpaceColonization.cs

protected void Grow(float dt)
{
    var kernel = compute.FindKernel("Grow");
    compute.SetBuffer(kernel, "_Nodes", nodeBuffer);

    var delta = dt * growthSpeed;
    compute.SetFloat("_DT", delta);

    GPUHelper.Dispatch1D(compute, kernel, count);
}

SpaceColonization.compute

void Grow (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;
  uint count, stride;
  _Nodes.GetDimensions(count, stride);
  if (idx >= count)
    return;

  Node n = _Nodes[idx];

  if (n.active)
  {
    // Disperse the growth rate with the mass parameter randomly set for each Node
    n.t = saturate(n.t + _DT * n.mass);
    _Nodes[idx] = n;
  }
}

1.3.3  Rendering

Now that we have a blanched shape with the above implementation, let's talk about how to render that shape.

Rendering with Line Topology

First, simply render using Line Mesh.

Generate a simple Line Topology Mesh to draw a Line that represents a single Edge.

SpaceColonization.cs

protected Mesh BuildSegment()
{
    var mesh = new Mesh ();
    mesh.hideFlags = HideFlags.DontSave;
    mesh.vertices = new Vector3[2] { Vector3.zero, Vector3.up };
    mesh.uv = new Vector2[2] { new Vector2(0f, 0f), new Vector2(0f, 1f) };
    mesh.SetIndices(new int[2] { 0, 1 }, MeshTopology.Lines, 0);
    return mesh;
}
Line Topology Mesh with only two simple vertices

Figure 1.9: Line Topology Mesh with only two simple vertices

Display the branches generated by rendering a Segment (line segment) with only two vertices using GPU instancing for the number of Edges.

SpaceColonization.cs

// Generate a buffer that determines the number of meshes to render, required for GPU instancing
protected void SetupDrawArgumentsBuffers(int count)
{
    if (drawArgs[1] == (uint)count) return;

    drawArgs[0] = segment.GetIndexCount(0);
    drawArgs[1] = (uint)count;

    if (drawBuffer != null) drawBuffer.Dispose();
    drawBuffer = new ComputeBuffer(
        1,
        sizeof(uint) * drawArgs.Length,
        ComputeBufferType.IndirectArguments
    );
    drawBuffer.SetData(drawArgs);
}

...

// Perform rendering with GPU instancing
protected void Render(float extents = 100f)
{
    block.SetBuffer("_Nodes", nodeBuffer);
    block.SetBuffer("_Edges", edgeBuffer);
    block.SetInt("_EdgesCount", edgesCount);
    block.SetMatrix("_World2Local", transform.worldToLocalMatrix);
    block.SetMatrix("_Local2World", transform.localToWorldMatrix);
    Graphics.DrawMeshInstancedIndirect(
        segment, 0,
        material, new Bounds(Vector3.zero, Vector3.one * extents),
        drawBuffer, 0, block
    );
}

The shader for rendering (Edge.shader) generates an animation of the branch extending from the branch point by controlling the length of the Edge according to the growth rate parameter (t) of the Node.

Edge.shader

v2f vert(appdata IN, uint iid : SV_InstanceID)
{
  v2f OUT;
  UNITY_SETUP_INSTANCE_ID(IN);
  UNITY_TRANSFER_INSTANCE_ID(IN, OUT);

  // Get the corresponding Edge from the instance ID
  Edge e = _Edges[iid];

  // Get 2 Nodes from the index of Edge
  Node a = _Nodes[e.a];
  Node b = _Nodes [eb];

  float3 ap = a.position;
  float3 bp = b.position;
  float3 dir = bp - ap;

  // Determine the length of Edge from a to b according to the growth rate (t) of Node b
  bp = ap + normalize (dir) * length (dir) * bt;

  // Since the vertex ID (IN.vid) is 0 or 1, if it is 0, it refers to the node of a, and if it is 1, it refers to the position of Node of b.
  float3 position = lerp(ap, bp, IN.vid);

  float4 vertex = float4(position, 1);
  OUT.position = UnityObjectToClipPos(vertex);
  OUT.uv = IN.uv;

  // If Node is inactive or the instance ID is outside the total number of Edges, set alpha to 0 and do not draw
  OUT.alpha = (a.active && b.active) && (iid < _EdgesCount);

  return OUT;
}

With these implementations, the shape obtained by the Space Colonization Algorithm can be rendered using Line Topology. You can get the following picture by executing Line.scene.

Line.scene --Example of rendering by Edge.shader

Figure 1.10: Line.scene --Example of rendering with Edge.shader

Rendering with Geometry Shader

By converting the Line Topology Segment to a Capsule shape with the Geometry Shader, you can draw thick lines.

Convert Line Topology Segment to Capsule shape with Geometry Shader

Figure 1.11: Convert Line Topology Segment to Capsule shape with Geometry Shader

The vertex shader is almost the same as Edge.shader, and Geometry Shader builds the Capsule shape. Only the important Geometry Shader implementations are listed below.

TubularEdge.shader

...
[maxvertexcount(64)]
void geom(line v2g IN[2], inout TriangleStream<g2f> OUT) {
  v2g p0 = IN[0];
  v2g p1 = IN[1];

  float alpha = p0.alpha;

  float3 t = normalize(p1.position - p0.position);
  float3 n = normalize(p0.viewDir);
  float3 bn = cross(t, n);
  n = cross(t, bn);

  float3 tp = lerp(p0.position, p1.position, alpha);
  float thickness = _Thickness * alpha;

  // Definition of Capsule mesh resolution
  static const uint rows = 6, cols = 6;
  static const float rows_inv = 1.0 / rows, cols_inv = 1.0 / (cols - 1);

  g2f00, o1;
  o0.uv = p0.uv; o0.uv2 = p0.uv2;
  o1.uv = p1.uv; o1.uv2 = p1.uv2;

  // Build aspects of the Capsule
  for (uint i = 0; i < cols; i++) {
    float r = (i * cols_inv) * UNITY_TWO_PI;

    float s, c;
    sincos(r, s, c);
    float3 normal = normalize(n * c + bn * s);

    float3 w0 = p0.position + normal * thickness;
    float3 w1 = p1.position + normal * thickness;
    o0.normal = o1.normal = normal;

    o0.position = UnityWorldToClipPos(w0);
    OUT.Append(o0);

    o1.position = UnityWorldToClipPos(w1);
    OUT.Append(o1);
  }
  OUT.RestartStrip();

  // Construction of Capsule tip (hemispherical)
  uint row, col;
  for (row = 0; row < rows; row++)
  {
    float s0 = sin((row * rows_inv) * UNITY_HALF_PI);
    float s1 = sin(((row + 1) * rows_inv) * UNITY_HALF_PI);
    for (col = 0; col < cols; col++)
    {
      float r = (col * cols_inv) * UNITY_TWO_PI;

      float s, c;
      sincos(r, s, c);

      float3 n0 = normalize(n * c * (1.0 - s0) + bn * s * (1.0 - s0) + t * s0);
      float3 n1 = normalize(n * c * (1.0 - s1) + bn * s * (1.0 - s1) + t * s1);

      o0.position = UnityWorldToClipPos(float4(tp + n0 * thickness, 1));
      o0.normal = n0;
      OUT.Append(o0);

      o1.position = UnityWorldToClipPos(float4(tp + n1 * thickness, 1));
      o1.normal = n1;
      OUT.Append(o1);
    }
    OUT.RestartStrip();
  }
}

...

The result looks like this: (TubularEdge.scene)

TubularEdge.scene --Example of rendering by TubularEdge.shader

Figure 1.12: TubularEdge.scene --Example of rendering with TubularEdge.shader

Now that you can render Edge with a thick mesh, you can add lighting and so on.

1.4  Application

With the above, we have realized the GPU implementation of Space Colonization. In this section, we will introduce the cooperation with skinning animation as an application example.

With this application, it is possible to realize an expression that blanchs along the animated model shape.

1.4.1  Rough flow

The cooperation with skinning animation is developed according to the following flow.

  1. Prepare an animation model
  2. Prepare a point cloud that fills the volume of the model (point cloud that becomes an attraction)
  3. Give Attraction and Node Bone information
  4. Apply (skinning) Bone transformation to Node to change its position

1.4.2  Preparation of resources

The structure of the previous example

Make changes to have the Bone index.

About Bone Limits Affected

In this application, the number of bones that affect each node is limited to one. Originally, skinning animation could have multiple bones affecting each vertex, but in this example we simply limit it to only one.

SkinnedAttraction.cs

public struct SkinnedAttraction {
    public Vector3 position;
    public int bone; // boneのindex
    public int nearest;
    public uint found;
    public uint active;
}

SkinnedNode.cs

public struct SkinnedNode {
    public Vector3 position;
    public Vector3 animated; // Node position after skinning animation
    public int index0; // boneのindex
    public float t;
    public float offset;
    public float mass;
    public int from;
    public uint active;
}

SkinnedCandidate.cs

public struct SkinnedCandidate
{
    public Vector3 position;
    public int node;
    public int bone; // boneのindex
}

Animation model and volume

Prepare the animation model you want to link.

In this example, the model downloaded from Clara.io * 2 is used (the number of polygons is reduced by reduction with MeshLab * 3 ), and the animation is generated by mixamo * 4 .

[*2] https://clara.io/view/d49ee603-8e6c-4720-bd20-9e3d7b13978a

[*3] http://www.meshlab.net/

[*4] https://mixamo.com

In order to get the position of Attraction from the model volume, we use a package called VolumeSampler * 5 that generates a point cloud in the model volume .

[*5] https://github.com/mattatz/unity-volume-sampler

VolumeSampler

VolumeSampler acquires the volume of the model using the technique explained in Unity Graphics Programming vol.2 "Real-Time GPU-Based Voxelizer". First, the volume inside the mesh is acquired as Voxel, and Poisson Disk Sampling is executed based on it to generate a point cloud that fills the inside of the mesh.

To generate a point cloud asset using VolumeSampler, click Window → VolumeSampler from the Unity toolbar to display Window, and as shown in the figure below.

If you set and click the asset generation button, the Volume asset will be generated in the specified path.

VolumeSamplerWindow

図1.13: VolumeSamplerWindow

Generate a SkinnedAttraction array from the point cloud asset (Volume class) generated from VolumeSampler and apply it to ComputeBuffer.

SkinnedSpaceColonization.cs

protected void Start() {
    ...
    // Generate a Skinned Attraction array from the point cloud of Volume
    attractions = GenerateAttractions(volume);
    count = attractions.Length;
    attractionBuffer = new ComputeBuffer(
        count,
        Marshal.SizeOf(typeof(SkinnedAttraction)),
        ComputeBufferType.Default
    );
    attractionBuffer.SetData(attractions);
    ...
}

Applying Bone information

Bone information of the nearest vertex from each position is applied to Skinned Attraction generated from the volume of Mesh.

The SetupSkin function prepares the vertices of the mesh and the bone buffer, and assigns the bone index to all Skinned Attraction on the GPU.

SkinnedSpaceColonization.cs

protected void Start() {
    ...
    SetupSkin();
    ...
}

...

protected void SetupSkin()
{
    var mesh = skinnedRenderer.sharedMesh;
    var vertices = mesh.vertices;
    var weights = mesh.boneWeights;
    var indices = new int[weights.Length];
    for(int i = 0, n = weights.Length; i < n; i++)
        indices[i] = weights[i].boneIndex0;

    using (
        ComputeBuffer
        vertBuffer = new ComputeBuffer(
            vertices.Length,
            Marshal.SizeOf(typeof(Vector3))
        ),
        boneBuffer = new ComputeBuffer(
            weights.Length,
            Marshal.SizeOf(typeof(uint))
        )
    )
    {
        vertBuffer.SetData(vertices);
        boneBuffer.SetData(indices);

        var kernel = compute.FindKernel("SetupSkin");
        compute.SetBuffer(kernel, "_Vertices", vertBuffer);
        compute.SetBuffer(kernel, "_Bones", boneBuffer);
        compute.SetBuffer(kernel, "_Attractions", attractionBuffer);
        GPUHelper.Dispatch1D(compute, kernel, attractionBuffer.count);
    }
}

Below is the implementation of the GPU kernel.

SkinnedSpaceColonization.compute

void SetupSkin (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;
  uint count, stride;
  _Attractions.GetDimensions(count, stride);
  if (idx >= count)
    return;

  SkinnedAttraction attr = _Attractions[idx];

  // Get the index of the nearest vertex from the position of Skinned Attraction
  float3 p = attr.position;
  uint closest = -1;
  float dist = 1e8;
  _Vertices.GetDimensions(count, stride);
  for (uint i = 0; i < count; i++)
  {
    float3 v = _Vertices[i];
    float l = distance(v, p);
    if (l < dist)
    {
      dist = l;
      closest = i;
    }
  }

  // Set the bone index of the nearest vertex to Skinned Attraction
  attr.bone = _Bones[closest];
  _Attractions[idx] = attr;
}

1.4.3  Implementation of algorithm in Compute Shader

In this application, some GPU kernels are modified to get the bone information needed for skinning animation in each step of the Space Colonization Algorithm.

The contents of the GPU kernel are almost the same, but for the generated SkinnedNode, it is necessary to obtain Bone information (Bone index) from the nearest Skinned Attraction, so

In the two GPU kernels, neighborhood search logic has been added.

SkinnedSpaceColonization.compute

void Seed (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;

  uint count, stride;
  _Seeds.GetDimensions(count, stride);
  if (idx >= count)
    return;

  SkinnedNode n;
  uint i = CreateNode(n);
  n.position = n.animated = _Seeds[idx];
  n.t = 1;
  n.offset = 0;
  n.from = -1;
  n.mass = lerp(_MassMin, _MassMax, nrand(id.xy));

  // Search for the nearest Skinned Attraction and
  // Copy the Bone index
  uint nearest = -1;
  float dist = 1e8;
  _Attractions.GetDimensions(count, stride);
  for (uint j = 0; j < count; j++)
  {
    SkinnedAttraction attr = _Attractions[j];
    float l = distance(attr.position, n.position);
    if (l < dist)
    {
      nearest = j;
      dist = l;
    }
  }
  n.index0 = _Attractions[nearest].bone;

  _Nodes[i] = n;
}

...

void Attract (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;
  uint count, stride;
  _Nodes.GetDimensions(count, stride);
  if (idx >= count)
    return;

  SkinnedNode n = _Nodes[idx];

  if (n.active && n.t >= 1.0)
  {
    float3 dir = (0.0).xxx;
    uint counter = 0;

    float dist = 1e8;
    uint nearest = -1;

    _Attractions.GetDimensions(count, stride);
    for (uint i = 0; i < count; i++)
    {
      SkinnedAttraction attr = _Attractions[i];
      if (attr.active && attr.found && attr.nearest == idx)
      {
        float3 dir2 = (attr.position - n.position);
        dir + = normalize (dir2);
        counter++;

        // Search for the nearest Skinned Attraction
        float l2 = length(dir2);
        if (l2 < dist)
        {
          dist = l2;
          nearest = i;
        }
      }
    }

    if (counter > 0)
    {
      SkinnedCandidate c;
      dir = dir / counter;
      c.position = n.position + (dir * _GrowthDistance);
      c.node = idx;
      // Set the bone index of the nearest Skinned Attraction
      c.bone = _Attractions[nearest].bone;
      _CandidatesAppend.Append(c);
    }
  }
}

Skinning animation

With the above implementation, you can now execute the Space Colonization Algorithm while setting the Bone information for Node.

After that, you can apply skinning animation to Node by getting the required Bone matrix from SkinnedMeshRenderer and moving the position of SkinnedNode on the GPU according to the deformation of Bone.

SkinnedSpaceColonization.cs

protected void Start() {
    ...
    // Create a buffer for the bind pose matrix
    var bindposes = skinnedRenderer.sharedMesh.bindposes;
    bindPoseBuffer = new ComputeBuffer(
        bindposes.Length,
        Marshal.SizeOf(typeof(Matrix4x4))
    );
    bindPoseBuffer.SetData(bindposes);
    ...
}

protected void Animate()
{
    // Create a buffer representing the SkinnedMeshRenderer's Bone matrix that is updated as the animation plays
    var bones = skinnedRenderer.bones.Select(bone => {
        return bone.localToWorldMatrix;
    }).ToArray();
    using (
        ComputeBuffer boneMatrixBuffer = new ComputeBuffer(
            bones.Length,
            Marshal.SizeOf(typeof(Matrix4x4))
        )
    )
    {
        boneMatrixBuffer.SetData(bones);

        // Pass the Bone and Node buffers and perform GPU skinning
        var kernel = compute.FindKernel("Animate");
        compute.SetBuffer(kernel, "_BindPoses", bindPoseBuffer);
        compute.SetBuffer(kernel, "_BoneMatrices", boneMatrixBuffer);
        compute.SetBuffer(kernel, "_Nodes", nodeBuffer);
        GPUHelper.Dispatch1D(compute, kernel, count);
    }
}

SkinnedSpaceColonization.compute

void Animate (uint3 id : SV_DispatchThreadID)
{
  uint idx = id.x;
  uint count, stride;
  _Nodes.GetDimensions(count, stride);
  if (idx >= count)
    return;

  SkinnedNode node = _Nodes[idx];
  if (node.active)
  {
    // Perform skinning
    float4x4 bind = _BindPoses[node.index0];
    float4x4 m = _BoneMatrices[node.index0];
    node.animated = mul(mul(m, bind), float4(node.position, 1)).xyz;
    _Nodes[idx] = node;
  }
}

1.4.4  Rendering

The shaders for rendering are almost the same, except that the Edge is drawn by referring to the animated position after skinning animation, not the original position of the SkinnedNode.

SkinnedTubularEdge.hlsl

v2g vert(appdata IN, uint iid : SV_InstanceID)
{
  ...
  Edge e = _Edges[iid];

  // Refer to the position after applying skinning animation
  SkinnedNode a = _Nodes[e.a], b = _Nodes[e.b];
  float3 ap = a.animated, bp = b.animated;

  float3 dir = bp - ap;
  bp = ap + normalize (dir) * length (dir) * bt;
  float3 position = lerp(ap, bp, IN.vid);
  OUT.position = mul(unity_ObjectToWorld, float4(position, 1)).xyz;
  ...
}

With the above implementation, you can get the capture picture shown at the beginning. ( Fig. 1.1 SkinnedAnimation.scene)

1.5  Summary

In this chapter, we introduced the GPU implementation of the Space Colonization Algorithm that generates a blanching shape along a point cloud, and an application example that combines it with skinning animation.

With this technique,

You can control the density of branches with these three parameters, but you can generate more diverse models by changing these parameters locally or with time.

Also, in the sample, Attraction runs the algorithm only with what was generated during initialization, but you should be able to generate more different patterns by dynamically increasing Attraction.

If you are interested, try various applications of this algorithm to find interesting patterns.

1.6  Reference